Bahasa Indonesia

Buka potensi multithreading sejati di JavaScript. Panduan komprehensif ini membahas SharedArrayBuffer, Atomics, Web Worker, dan persyaratan keamanan untuk aplikasi web performa tinggi.

JavaScript SharedArrayBuffer: Menyelami Pemrograman Konkuren di Web Secara Mendalam

Selama beberapa dekade, sifat single-threaded JavaScript telah menjadi sumber kesederhanaannya sekaligus hambatan performa yang signifikan. Model event loop bekerja dengan sangat baik untuk sebagian besar tugas yang digerakkan oleh UI, tetapi kesulitan saat dihadapkan pada operasi yang intensif secara komputasi. Kalkulasi yang berjalan lama dapat membekukan browser, menciptakan pengalaman pengguna yang membuat frustrasi. Meskipun Web Worker menawarkan solusi parsial dengan memungkinkan skrip berjalan di latar belakang, mereka datang dengan batasan utama mereka sendiri: komunikasi data yang tidak efisien.

Masuklah SharedArrayBuffer (SAB), sebuah fitur canggih yang secara fundamental mengubah permainan dengan memperkenalkan berbagi memori tingkat rendah yang sebenarnya antar-thread di web. Dipasangkan dengan objek Atomics, SAB membuka era baru aplikasi berkinerja tinggi dan konkuren langsung di browser. Namun, dengan kekuatan besar datang pula tanggung jawab besar—dan kompleksitas.

Panduan ini akan membawa Anda menyelami dunia pemrograman konkuren di JavaScript. Kita akan menjelajahi mengapa kita membutuhkannya, cara kerja SharedArrayBuffer dan Atomics, pertimbangan keamanan kritis yang harus Anda atasi, dan contoh praktis untuk memulai.

Dunia Lama: Model Single-Threaded JavaScript dan Keterbatasannya

Sebelum kita dapat menghargai solusinya, kita harus sepenuhnya memahami masalahnya. Eksekusi JavaScript di browser secara tradisional terjadi pada satu thread, sering disebut "main thread" atau "UI thread".

Event Loop

Main thread bertanggung jawab untuk segalanya: mengeksekusi kode JavaScript Anda, merender halaman, merespons interaksi pengguna (seperti klik dan scroll), dan menjalankan animasi CSS. Ia mengelola tugas-tugas ini menggunakan event loop, yang secara terus-menerus memproses antrean pesan (tugas). Jika sebuah tugas memakan waktu lama untuk selesai, ia akan memblokir seluruh antrean. Tidak ada hal lain yang bisa terjadi—UI membeku, animasi tersendat, dan halaman menjadi tidak responsif.

Web Worker: Sebuah Langkah ke Arah yang Benar

Web Worker diperkenalkan untuk mengatasi masalah ini. Sebuah Web Worker pada dasarnya adalah skrip yang berjalan di thread latar belakang yang terpisah. Anda dapat memindahkan komputasi berat ke worker, menjaga main thread tetap bebas untuk menangani antarmuka pengguna.

Komunikasi antara main thread dan worker terjadi melalui API postMessage(). Ketika Anda mengirim data, data tersebut ditangani oleh algoritma structured clone. Ini berarti data diserialisasi, disalin, dan kemudian dideserialisasi dalam konteks worker. Meskipun efektif, proses ini memiliki kelemahan signifikan untuk dataset besar:

Bayangkan sebuah editor video di browser. Mengirim seluruh frame video (yang bisa berukuran beberapa megabyte) bolak-balik ke worker untuk diproses 60 kali per detik akan sangat mahal. Inilah masalah yang dirancang untuk dipecahkan oleh SharedArrayBuffer.

Sang Pengubah Permainan: Memperkenalkan SharedArrayBuffer

Sebuah SharedArrayBuffer adalah buffer data biner mentah dengan panjang tetap, mirip dengan ArrayBuffer. Perbedaan kritisnya adalah SharedArrayBuffer dapat dibagikan di beberapa thread (misalnya, main thread dan satu atau lebih Web Worker). Ketika Anda "mengirim" SharedArrayBuffer menggunakan postMessage(), Anda tidak mengirim salinan; Anda mengirim referensi ke blok memori yang sama.

Ini berarti setiap perubahan yang dibuat pada data buffer oleh satu thread akan langsung terlihat oleh semua thread lain yang memiliki referensi ke sana. Ini menghilangkan langkah salin-dan-serialisasi yang mahal, memungkinkan berbagi data yang hampir seketika.

Anggap saja seperti ini:

Bahaya Memori Bersama: Kondisi Balapan (Race Conditions)

Berbagi memori secara instan memang kuat, tetapi juga memperkenalkan masalah klasik dari dunia pemrograman konkuren: kondisi balapan (race conditions).

Kondisi balapan terjadi ketika beberapa thread mencoba mengakses dan memodifikasi data bersama yang sama secara bersamaan, dan hasil akhirnya bergantung pada urutan eksekusi mereka yang tidak dapat diprediksi. Pertimbangkan sebuah penghitung sederhana yang disimpan dalam SharedArrayBuffer. Baik main thread maupun worker ingin menaikkan nilainya.

  1. Thread A membaca nilai saat ini, yaitu 5.
  2. Sebelum Thread A dapat menulis nilai baru, sistem operasi menjedanya dan beralih ke Thread B.
  3. Thread B membaca nilai saat ini, yang masih 5.
  4. Thread B menghitung nilai baru (6) dan menuliskannya kembali ke memori.
  5. Sistem beralih kembali ke Thread A. Ia tidak tahu bahwa Thread B melakukan apa pun. Ia melanjutkan dari tempat ia berhenti, menghitung nilai barunya (5 + 1 = 6) dan menulis 6 kembali ke memori.

Meskipun penghitung dinaikkan dua kali, nilai akhirnya adalah 6, bukan 7. Operasi tersebut tidak atomik—mereka dapat diinterupsi, yang menyebabkan hilangnya data. Inilah alasan mengapa Anda tidak dapat menggunakan SharedArrayBuffer tanpa mitra krusialnya: objek Atomics.

Sang Penjaga Memori Bersama: Objek Atomics

Objek Atomics menyediakan serangkaian metode statis untuk melakukan operasi atomik pada objek SharedArrayBuffer. Sebuah operasi atomik dijamin akan dilakukan secara keseluruhan tanpa diinterupsi oleh operasi lain. Entah itu terjadi sepenuhnya atau tidak sama sekali.

Menggunakan Atomics mencegah kondisi balapan dengan memastikan bahwa operasi baca-modifikasi-tulis pada memori bersama dilakukan dengan aman.

Metode Kunci Atomics

Mari kita lihat beberapa metode paling penting yang disediakan oleh Atomics.

Sinkronisasi: Melampaui Operasi Sederhana

Terkadang Anda membutuhkan lebih dari sekadar membaca dan menulis yang aman. Anda perlu agar thread dapat berkoordinasi dan menunggu satu sama lain. Anti-pola yang umum adalah "busy-waiting," di mana sebuah thread berada dalam loop ketat, terus-menerus memeriksa lokasi memori untuk perubahan. Ini membuang-buang siklus CPU dan menguras masa pakai baterai.

Atomics menyediakan solusi yang jauh lebih efisien dengan wait() dan notify().

Menyatukan Semuanya: Panduan Praktis

Sekarang setelah kita memahami teorinya, mari kita ikuti langkah-langkah mengimplementasikan solusi menggunakan SharedArrayBuffer.

Langkah 1: Prasyarat Keamanan - Isolasi Lintas-Asal (Cross-Origin Isolation)

Ini adalah batu sandungan paling umum bagi para pengembang. Untuk alasan keamanan, SharedArrayBuffer hanya tersedia di halaman yang berada dalam status terisolasi lintas-asal (cross-origin isolated). Ini adalah tindakan keamanan untuk mengurangi kerentanan eksekusi spekulatif seperti Spectre, yang berpotensi menggunakan timer beresolusi tinggi (yang dimungkinkan oleh memori bersama) untuk membocorkan data antar-asal.

Untuk mengaktifkan isolasi lintas-asal, Anda harus mengonfigurasi server web Anda untuk mengirim dua header HTTP spesifik untuk dokumen utama Anda:


Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp

Ini bisa menjadi tantangan untuk diatur, terutama jika Anda bergantung pada skrip atau sumber daya pihak ketiga yang tidak menyediakan header yang diperlukan. Setelah mengonfigurasi server Anda, Anda dapat memverifikasi apakah halaman Anda terisolasi dengan memeriksa properti self.crossOriginIsolated di konsol browser. Nilainya harus true.

Langkah 2: Membuat dan Berbagi Buffer

Di skrip utama Anda, Anda membuat SharedArrayBuffer dan "tampilan" (view) di atasnya menggunakan TypedArray seperti Int32Array.

main.js:


// Cek isolasi lintas-asal terlebih dahulu!
if (!self.crossOriginIsolated) {
  console.error("Halaman ini tidak terisolasi lintas-asal. SharedArrayBuffer tidak akan tersedia.");
} else {
  // Buat buffer bersama untuk satu integer 32-bit.
  const buffer = new SharedArrayBuffer(4);

  // Buat tampilan (view) di atas buffer. Semua operasi atomik terjadi pada tampilan ini.
  const int32Array = new Int32Array(buffer);

  // Inisialisasi nilai pada indeks 0.
  int32Array[0] = 0;

  // Buat worker baru.
  const worker = new Worker('worker.js');

  // Kirim buffer BERSAMA ke worker. Ini adalah transfer referensi, bukan salinan.
  worker.postMessage({ buffer });

  // Dengarkan pesan dari worker.
  worker.onmessage = (event) => {
    console.log(`Worker melaporkan selesai. Nilai akhir: ${Atomics.load(int32Array, 0)}`);
  };
}

Langkah 3: Melakukan Operasi Atomik di Worker

Worker menerima buffer dan sekarang dapat melakukan operasi atomik padanya.

worker.js:


self.onmessage = (event) => {
  const { buffer } = event.data;
  const int32Array = new Int32Array(buffer);

  console.log("Worker menerima buffer bersama.");

  // Mari lakukan beberapa operasi atomik.
  for (let i = 0; i < 1000000; i++) {
    // Naikkan nilai bersama dengan aman.
    Atomics.add(int32Array, 0, 1);
  }

  console.log("Worker selesai menaikkan nilai.");

  // Beri sinyal kembali ke main thread bahwa kita sudah selesai.
  self.postMessage({ done: true });
};

Langkah 4: Contoh Lebih Lanjut - Penjumlahan Paralel dengan Sinkronisasi

Mari kita selesaikan masalah yang lebih realistis: menjumlahkan array angka yang sangat besar menggunakan beberapa worker. Kita akan menggunakan Atomics.wait() dan Atomics.notify() untuk sinkronisasi yang efisien.

Buffer bersama kita akan memiliki tiga bagian:

main.js:


if (self.crossOriginIsolated) {
  const NUM_WORKERS = 4;
  const DATA_SIZE = 10_000_000;

  // [status, workers_selesai, hasil]
  // Kita menggunakan dua integer 32-bit untuk hasil agar tidak terjadi overflow untuk jumlah besar.
  const sharedBuffer = new SharedArrayBuffer(4 * 4); // 4 integer
  const sharedArray = new Int32Array(sharedBuffer);

  // Buat beberapa data acak untuk diproses
  const data = new Uint8Array(DATA_SIZE);
  for (let i = 0; i < DATA_SIZE; i++) {
    data[i] = Math.floor(Math.random() * 10);
  }

  const chunkSize = Math.ceil(DATA_SIZE / NUM_WORKERS);

  for (let i = 0; i < NUM_WORKERS; i++) {
    const worker = new Worker('sum_worker.js');
    const start = i * chunkSize;
    const end = Math.min(start + chunkSize, DATA_SIZE);
    
    // Buat tampilan yang tidak dibagikan untuk potongan data worker
    const dataChunk = data.subarray(start, end);

    worker.postMessage({ 
      sharedBuffer,
      dataChunk // Ini disalin
    });
  }

  console.log('Main thread sekarang menunggu para worker selesai...');

  // Tunggu bendera status di indeks 0 menjadi 1
  // Ini jauh lebih baik daripada loop while!
  Atomics.wait(sharedArray, 0, 0); // Tunggu jika sharedArray[0] adalah 0

  console.log('Main thread dibangunkan!');
  const finalSum = Atomics.load(sharedArray, 2);
  console.log(`Jumlah total paralel adalah: ${finalSum}`);

} else {
  console.error('Halaman tidak terisolasi lintas-asal.');
}

sum_worker.js:


self.onmessage = ({ data }) => {
  const { sharedBuffer, dataChunk } = data;
  const sharedArray = new Int32Array(sharedBuffer);

  // Hitung jumlah untuk potongan data worker ini
  let localSum = 0;
  for (let i = 0; i < dataChunk.length; i++) {
    localSum += dataChunk[i];
  }

  // Secara atomik tambahkan jumlah lokal ke total bersama
  Atomics.add(sharedArray, 2, localSum);

  // Secara atomik naikkan penghitung 'worker selesai'
  const finishedCount = Atomics.add(sharedArray, 1, 1) + 1;

  // Jika ini adalah worker terakhir yang selesai...
  const NUM_WORKERS = 4; // Sebaiknya dilewatkan sebagai parameter di aplikasi nyata
  if (finishedCount === NUM_WORKERS) {
    console.log('Worker terakhir selesai. Memberi tahu main thread.');

    // 1. Atur bendera status ke 1 (selesai)
    Atomics.store(sharedArray, 0, 1);

    // 2. Beri tahu main thread, yang sedang menunggu di indeks 0
    Atomics.notify(sharedArray, 0, 1);
  }
};

Kasus Penggunaan dan Aplikasi di Dunia Nyata

Di mana teknologi yang kuat namun kompleks ini benar-benar membuat perbedaan? Ia unggul dalam aplikasi yang memerlukan komputasi berat yang dapat diparalelkan pada dataset besar.

Tantangan dan Pertimbangan Akhir

Meskipun SharedArrayBuffer bersifat transformatif, ini bukanlah solusi untuk semua masalah. Ini adalah alat tingkat rendah yang memerlukan penanganan yang hati-hati.

  1. Kompleksitas: Pemrograman konkuren terkenal sulit. Melakukan debug pada kondisi balapan dan deadlock bisa sangat menantang. Anda harus berpikir secara berbeda tentang bagaimana status aplikasi Anda dikelola.
  2. Deadlock: Deadlock terjadi ketika dua atau lebih thread diblokir selamanya, masing-masing menunggu yang lain untuk melepaskan sumber daya. Ini bisa terjadi jika Anda mengimplementasikan mekanisme penguncian yang kompleks secara tidak benar.
  3. Beban Keamanan: Persyaratan isolasi lintas-asal adalah rintangan yang signifikan. Hal ini dapat merusak integrasi dengan layanan pihak ketiga, iklan, dan gateway pembayaran jika mereka tidak mendukung header CORS/CORP yang diperlukan.
  4. Bukan untuk Setiap Masalah: Untuk tugas latar belakang sederhana atau operasi I/O, model Web Worker tradisional dengan postMessage() seringkali lebih sederhana dan cukup. Gunakan SharedArrayBuffer hanya ketika Anda memiliki hambatan yang jelas dan terikat CPU yang melibatkan data dalam jumlah besar.

Kesimpulan

SharedArrayBuffer, bersama dengan Atomics dan Web Worker, mewakili pergeseran paradigma untuk pengembangan web. Ini menghancurkan batasan model single-threaded, mengundang kelas baru aplikasi yang kuat, berkinerja, dan kompleks ke dalam browser. Ini menempatkan platform web pada pijakan yang lebih setara dengan pengembangan aplikasi asli untuk tugas-tugas yang intensif secara komputasi.

Perjalanan menuju JavaScript konkuren sangat menantang, menuntut pendekatan yang ketat terhadap manajemen state, sinkronisasi, dan keamanan. Tetapi bagi para pengembang yang ingin mendorong batas dari apa yang mungkin di web—dari sintesis audio real-time hingga rendering 3D yang kompleks dan komputasi ilmiah—menguasai SharedArrayBuffer bukan lagi sekadar pilihan; ini adalah keterampilan penting untuk membangun aplikasi web generasi berikutnya.